/** Representation of a "sector" of the wasteland.
@param seed The Rrng seed associated with this sector.
*/
function WorldMapSector(seed)
{
	this.seed = seed;
	this.moisture = 0.15;
	this.heat = 1.1;
	this.recalculateColor();
	this.character = " ";
	this.name = "desert floor";
	this.blockWalk = false;
	this.destination = null;
	this.colorize = true;
	this.fg = Color.white;
}
WorldMapSector.prototype =
{
	/** Recalculates the sector's color based on it's heat and moisture
	values.
	*/
	recalculateColor: function()
	{
		if(!this.colorize)
			return;
		var mf = Math.min(Math.max(this.moisture, 0), 0.33);
		this.bg = new Color(
			0.66 * this.heat,
			(0.33 + (0.33 - mf)) * this.heat,
			0
		);
	}
};

/** The representation of our "world" map of the wasteland.
@param buffer Destination screen buffer.
@param xOfs Screen X offset.
@param yOfs Screen Y offset.
@param width Width in sectors.
@param height Height in sectors.
@param seed Rrng seed used.
@param player The player's mobile.
*/
function WorldMap(buffer, xOfs, yOfs, width, height, seed, player)
{
	this.buffer = buffer;
	this.xOfs = xOfs;
	this.yOfs = yOfs;
	this.width = width;
	this.height = height;
	this.seed = seed;
	this.player = player;
	this.playerX = 0;
	this.playerY = 0;
	this._sectors = [];
	this._knownMap = [];
	this.riverName = "Ambling River";

	// Initial map generation
	this.generateMap();
}
WorldMap.prototype =
{
	serialize: function(s)
	{
		s.push(this.playerX, this.playerY);

		// Save visibility information
		var visRle = [];
		var lastValue = 0;
		var count = 0;
		for(i = 0; i < this._knownMap.length; ++i)
		{
			if(this._knownMap[i] == lastValue)
				++count;
			else
			{
				visRle.push(count);
				lastValue = this._knownMap[i];
				count = 1;
			}
		}
		visRle.push(count);
		s.push(visRle);
	},
	deserialize: function(s)
	{
		// Load visibility information.
		var visRle = s.pop();
		var i, j, visible = false, ofs = 0;
		for(i = 0; i < visRle.length; ++i)
		{
			for(j = 0; j < visRle[i]; ++j)
			{
				this._knownMap[ofs++] = visible ? 1 : 0;
			}
			visible = !visible;
		}
		
		this.playerY = s.pop();
		this.playerX = s.pop();
	},
	/** Generates the world map's sector information for the seed value.
	*/
	generateMap: function()
	{
		var self = this;
		
		// Function that spreads heat / moisture from a point
		function spread(x, y, heatMod, moistureMod, radius)
		{
			var ix, iy, ds, rs = radius * radius, d, sector, dbyr;
			
			for(iy = y - radius; iy <= y + radius; ++iy)
			{
				if(iy < 0 ||
					iy >= self.height)
					continue;
				for(ix = x - radius; ix <= x + radius; ++ix)
				{
					if(ix < 0 ||
						ix >= self.width)
						continue;
					ds = (x - ix) * (x - ix) + (y - iy) * (y - iy);
					if(ds <= rs)
					{
						d = Math.sqrt(ds);
						dbyr = 1 - (d / radius);
						sector = self._sectors[iy * self.width + ix];
						sector.moisture += moistureMod * dbyr;
						sector.heat += heatMod * dbyr;
					}
				}
			}
		}
		
		// Function that (tries) to find a free location near a point
		function freeLocationNear(x, y, checkNearDest)
		{
			var i, tx, ty, sector, testSector, ix, iy, bad;
			
			for(i = 0; i < 5; ++i)
			{
				tx = x + rng.next(0, 3) - 1;
				ty = y + rng.next(0, 3) - 1;
				sector = self.getSector(tx, ty);
				// We have a valid sector, let's make sure it's not too close
				// to another destination.
				if(sector != null &&
					!sector.blockWalk &&
					sector.destination == null)
				{
					if(!checkNearDest)
						return sector;
					bad = false;
					for(iy = ty - 2; iy <= ty + 2; ++iy)
					{
						for(ix = tx - 2; ix <= tx + 2; ++ix)
						{
							testSector = self.getSector(ix, iy);
							if(testSector != null &&
								testSector.destination != null)
								bad = true;
						}
					}
					if(bad)
						continue;
					return sector;
				}
			}
			return null;
		}
		
		var rng = new Rrng(this.seed);
		
		// Create the sector objects, assign the seed values and initialized the
		// known map.
		for(var i = 0; i < this.width * this.height; ++i)
		{
			this._sectors.push(new WorldMapSector(rng.next()));
			this._sectors[i].id = i;
			this._knownMap.push(0);
		}
				
		// Heat map
		var count = rng.next(4, 8);
		for(var i = 0; i < count; ++i)
		{
			var cx = rng.next(0, this.width);
			var cy = rng.next(0, this.height);
			var size = rng.next(3, 20);
			var heatMod = (rng.nextFloat() * 0.3) - 0.15;
			var moistureMod = -(heatMod + .05);
			spread(cx, cy, heatMod, moistureMod, size);
		}
		
		// River
		var riverLocs = [];
		var rx = Math.floor(this.width / 2);
		var dir = 3;
		var ry = 0;
		while(ry < this.height + 5)
		{
			// Plot the river segment
			var sector = this.getSector(rx, ry);
			if(sector != null)
			{
				sector.character = "~";
				sector.bg = new Color(0.33, 0.166, 1);
				sector.colorize = false;
				sector.blockWalk = true;
				sector.name = this.riverName;
				riverLocs.push(rx);
				riverLocs.push(ry);
			}
			spread(rx, ry, -0.015, 0.025, 6);

			// Move				
			dir += rng.next(0, 3) - 1;
			if(dir < 0)
				dir = 0;
			if(dir > 4)
				dir = 4;

			if(dir < 2)
				rx--;
			else if(dir > 2)
				rx++;
			if(dir > 0 &&
				dir < 4)
				ry++;
		}
		
		// Add mesas
		count = rng.next(10, 30);
		var mesaLocs = [];
		for(var i = 0; i < count; ++i)
		{
			var rx = rng.next(0, this.width);
			var ry = rng.next(0, this.height);
			var sector = this._sectors[ry * this.width + rx];
			if(!sector.blockWalk)
			{
				sector.blockWalk = true;
				sector.character = "M";
				sector.name = "high mesa";
				mesaLocs.push(rx);
				mesaLocs.push(ry);
			}
		}
		
		// Add the river town
		for(var i = 0; i < 10; ++i)
		{
			var rLoc = rng.next(0, Math.floor(riverLocs.length / 2));
			var tx = riverLocs[rLoc * 2];
			var ty = riverLocs[rLoc * 2 + 1];
			var sector = freeLocationNear(tx, ty, true);
			if(sector != null)
			{
				sector.character = "1";
				sector.fg = Color.black;
				sector.name = "Town";
				sector.destination = new SectorMap("Town", "town", rng.next(), sector.id, 1);
				SectorMap.registerMap(sector.destination);
				break;
			}
		}
		// Add the mesa town
		for(var i = 0; i < 10; ++i)
		{
			var rLoc = rng.next(0, Math.floor(mesaLocs.length / 2));
			var tx = mesaLocs[rLoc * 2];
			var ty = mesaLocs[rLoc * 2 + 1];
			var sector = freeLocationNear(tx, ty, true);
			if(sector != null)
			{
				sector.character = "2";
				sector.fg = Color.black;
				sector.name = "Town";
				sector.destination = new SectorMap("Town", "town", rng.next(), sector.id, 1);
				SectorMap.registerMap(sector.destination);
				break;
			}
		}
		// Add the (more) random town
		for(var i = 0; i < 10; ++i)
		{
			var tx = rng.next(0, this.width);
			var ty = rng.next(0, this.height);
			var sector = freeLocationNear(tx, ty, true);
			if(sector != null)
			{
				sector.character = "3";
				sector.fg = Color.black;
				sector.name = "Town";
				sector.destination = new SectorMap("Town", "town", rng.next(), sector.id, 1);
				SectorMap.registerMap(sector.destination);
				break;
			}
		}
		
		// Add points of interest
		count = rng.next(7, 13);
		var piIndex = 0;
		for(var i = 0; i < count; ++i)
		{
			for(var j = 0; j < 5; ++j)
			{
				var tx = rng.next(0, this.width);
				var ty = rng.next(0, this.height);
				var sector = freeLocationNear(tx, ty, true);
				if(sector != null)
				{
					sector.character = util.letters.charAt(piIndex++);
					sector.fg = Color.black;
					sector.name = "Point of Interest";
					sector.destination = new SectorMap("Point of Interest", rng.nextElement(SectorMap.pointOfInterestTypes), rng.next(), sector.id, 1);
					SectorMap.registerMap(sector.destination);
					break;
				}
			}
		}
		
		// Apply noise and recalculate colors, set heat / moisture for destinations
		for(var i = 0; i < this.width * this.height; ++i)
		{
			var sector = this._sectors[i];
			sector.heat += (rng.nextFloat() * 0.025) - 0.01;
			sector.moisture += (rng.nextFloat() * 0.05) - 0.025;
			sector.recalculateColor();
			if(sector.destination != null)
			{
				sector.destination.heat = sector.heat;
				sector.destination.moisture = sector.moisture;
			}
		}
	},
	/** Called whenever the player's turn is over.
	*/
	_doTurn: function()
	{
		// TODO
	},
	/** Update visibility information for the player.
	 */
	_updateVisibility: function()
	{
		var visDSqr = (this.player.worldMapVisRange + 0.25) * (this.player.worldMapVisRange + 0.25);
		var ix, iy, dx, dy, ofs;
		for(iy = this.player.y - this.player.worldMapVisRange; iy <= this.player.y + this.player.worldMapVisRange; ++iy)
		{
			for(ix = this.player.x - this.player.worldMapVisRange; ix <= this.player.x + this.player.worldMapVisRange; ++ix)
			{
				if(ix < 0 ||
					iy < 0 ||
					ix >= this.width ||
					iy >= this.width)
					continue;
				dx = this.player.x - ix;
				dx *= dx;
				dy = this.player.y - iy;
				dy *= dy;
				if(dx + dy > visDSqr)
					continue;
				ofs = iy * this.width + ix;
				if(this._knownMap[ofs] == 0)
				{
					this._knownMap[ofs] = 1;
					this.renderPosition(ix, iy);
				}
			}
		}
	},
	/** Called when the player re-enters the world map mode.
	@param player The player.
	*/
	onPlayerReenter: function(player)
	{
		this.player = player;
		player.parent = this;
		player.x = this.playerX;
		player.y = this.playerY;
		this._updateVisibility();
	},
	/** Try to use something at the player's position.
	*/
	tryUse: function()
	{
		var sector = this.getPlayerSector();
		if(sector != null &&
			sector.destination != null)
		{
			modeStack.push(new SectorMapMode(sector.destination));
			sector.destination.addPlayer(this.player, true);
			removeMode(worldMapMode);
		}
		else
			this._doTurn();
	},
	/** Try to move the player's position.
	@param x X offset.
	@param y Y offset.
	*/
	tryMovePlayer: function(x, y)
	{
		var nx = this.player.x + x;
		var ny = this.player.y + y;

		// Bounds check
		if(nx < 0 ||
			ny < 0 ||
			nx >= this.width ||
			ny >= this.height)
			return;

		// Walkability check
		var sector = this._sectors[ny * this.width + nx];
		if(sector.blockWalk)
			return;

		var ox = this.player.x;
		var oy = this.player.y;
		this.player.x += x;
		this.player.y += y;
		this.playerX = player.x;
		this.playerY = player.y;
		
		this._updateVisibility();
		this.renderPlayer();
		this.renderPosition(ox, oy);
		this._doTurn();
	},
	/** Get a sector (or null).
	@param x X position.
	@param y Y position.
	*/
	getSector: function(x, y)
	{
		if(x < 0 ||
			y < 0 ||
			x >= this.width ||
			y >= this.height)
			return null;
		return this._sectors[y * this.width + x];
	},
	/** Get the player's current sector.
	*/
	getPlayerSector: function()
	{
		var x = this.player.x;
		var y = this.player.y;
		if(x < 0 ||
			y < 0 ||
			x >= this.width ||
			y >= this.height)
			return null;
		return this._sectors[y * this.width + x];
	},
	/** Returns a description of the position.
	@param x X position.
	@param y Y position.
	*/
	getPositionDescription: function(x, y)
	{
		var sector = this.getSector(x, y);
		if(!sector ||
			this._knownMap[y * this.width + x] == 0)
			return "Unknown";
		return sector.name;
	},
	/** Draw a single sector.
	@param x X position.
	@param y Y position.
	*/
	renderPosition: function(x, y)
	{
		if(x < 0 ||
			y < 0 ||
			x >= this.width ||
			y >= this.height)
			return;
		var ofs = y * this.width + x;
		if(this._knownMap[ofs] == 0)
			this.buffer.setCell(x + this.xOfs, y + this.yOfs, " ", null, Color.black);
		else
		{
			var sector = this._sectors[ofs];
			this.buffer.setCell(x + this.xOfs, y + this.yOfs, sector.character, sector.fg, sector.bg);
			if(this.player.x == x &&
				this.player.y == y)
				this.renderPlayer();
		}
	},
	/** Draw the player's cursor.
	*/
	renderPlayer: function()
	{
		this.buffer.setCell(this.player.x + this.xOfs, this.player.y + this.yOfs, this.player.character, this.player.fg, this.player.bg);
	},
	/** Draw the map onto a screen buffer.
	*/
	render: function()
	{
		var ix, iy, sector, ofs;
		for(iy = 0; iy < this.height; ++iy)
		{
			for(ix = 0; ix < this.width; ++ix)
			{
				ofs = iy * this.width + ix;
				if(this._knownMap[ofs] == 0)
					this.buffer.setCell(ix + this.xOfs, iy + this.yOfs, " ", null, Color.black);
				else
				{
					sector = this._sectors[iy * this.width + ix];
					this.buffer.setCell(ix + this.xOfs, iy + this.yOfs, sector.character, sector.fg, sector.bg);
				}
			}
		}
		this.renderPlayer();
	},
	/** Draw an overlay on the map
	@param x X position.
	@param y Y position.
	@param c Character.
	@param fg Foreground color.
	@param bg Background color.
	*/
	renderOverlay: function(x, y, c, fg, bg)
	{
		if(x < 0 ||
			y < 0 ||
			x >= this.width ||
			y >= this.height)
			return;
		this.buffer.setCell(x + this.xOfs, y + this.yOfs, c, fg, bg);
	}
};
